﻿using System;
using System.Diagnostics.Contracts;
using System.Dynamic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;

namespace Isuka.Xml
{
    /// <summary>
    /// XElement を動的読み出しするためのラッパー。
    /// オリジナルは、http://ufcpp.net/study/csharp/source/DynamicXml.cs
    /// </summary>
    public class DynamicXElement
        : DynamicObject
    {
        readonly XElement el;
        readonly XNamespace ns;

        /// <summary>
        /// XElement を与えて初期化。
        /// </summary>
        /// <param name="element">読み取り対象の XElement。</param>
        public DynamicXElement(XElement element)
        {
            Contract.Requires<ArgumentNullException>(element != null, "element");

            el = element;
            ns = element.GetDefaultNamespace();
        }

        public DynamicXElement(XmlReader reader)
        {
            Contract.Requires<ArgumentNullException>(reader != null, "reader");

            el = XElement.Load(reader);
            ns = el.GetDefaultNamespace();
        }

        public XElement BaseElement
        {
            get { return el; }
        }

        public XNamespace DefaultNamespace
        {
            get { return ns; }
        }

        [ContractInvariantMethod]
        void ObjectInvariant()
        {
            Contract.Invariant(el != null);
            Contract.Invariant(ns != null);
        }

        #region Overrides DynamicObject

        public override bool TryGetMember(System.Dynamic.GetMemberBinder binder, out object result)
        {
            Contract.Assume(binder != null);

            var name = binder.Name;
            if (name == "_")    // _ だけの場合は、DynamicXAttributes を返す
            {
                result = new DynamicXAttributes(el);
                return true;
            }
            else if (name.StartsWith("@") && 1 < name.Length)  // @スタートなら属性。_.AttrName と同じ
            {
                var attributes = el.Attributes(name.Substring(1));
                var attrValues = attributes.Select(x => x.Value);
                result = attrValues;
                return true;
            }
            else
            {
                var xName = ns.GetName(name);
                var subElements = el.Elements(xName).Select(x => new DynamicXElement(x)).ToArray();
                if (subElements.Length == 0)
                {
                    result = null;
                    return true;
                }
                else if (subElements.Length == 1)
                {
                    result = subElements[0];
                    return true;
                }
                else
                {
                    result = subElements;
                    return true;
                }
            }
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            Contract.Assume(binder != null);

            if (binder.Type == typeof(XElement))
            {
                result = el;
                return true;
            }
            else if (binder.Type == typeof(string))
            {
                result = el.Value;
                return true;
            }
            else if (binder.Type == typeof(int))
            {
                result = int.Parse(el.Value);
                return true;
            }
            else if (binder.Type == typeof(double))
            {
                result = double.Parse(el.Value);
                return true;
            }
            else if (binder.Type == typeof(bool))
            {
                result = bool.Parse(el.Value);
                return true;
            }
            else if (binder.Type == typeof(System.Collections.IEnumerable))
            {
                result = new[] { this };
                return true;
            }
            else
            {
                return base.TryConvert(binder, out result);
            }
        }

        public override bool TryInvokeMember(System.Dynamic.InvokeMemberBinder binder, object[] args, out object result)
        {
            Contract.Assume(binder != null);

            switch (binder.Name)
            {
                case "GetEnumerator":
                    result = new[] { this }.AsEnumerable().GetEnumerator();
                    return true;

                case "All":
                    result = el.Elements().Select(x => new DynamicXElement(x));
                    return true;

                case "Attributes":
                    result = new DynamicXAttributes(el);
                    return true;

                case "Name":
                    result = el.Name.ToString();
                    return true;
            }

            return base.TryInvokeMember(binder, args, out result);
        }

        public override string ToString()
        {
            return el.ToString();
        }

        #endregion
    }
}
